/**
 * DSM - Distributed Search Manager
 * Developed by Milspec Research International Pty Ltd
 * $Author: skahl $
 * $Revision: 1.8 $
 * $Date: 2007/09/28 00:23:37 $
 * (c)Copyright 2004
 * education.au limited
 * DEST
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the names education.au limited, DEST nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 * To the extent permitted by law, the copyright owners of this software and
 * its contributors:
 * (i) exclude all warranties in relation to the software; and
 * (ii) exclude liability for all losses, costs, expenses and damages arising
 * in any way from the use of the software whether arising from or in
 * relation to breach of contract, negligence or any other tort, in equity
 * or otherwise. If the software is in breach of a warranty which is
 * implied by law, the copyright owners and contributors liability is
 * limited to the replacement of the software.
 *
 * @author	gsingh
 */

package au.edu.educationau.opensource.dsm.obj;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.math.NumberUtils;

import au.edu.educationau.opensource.dsm.service.PropertiesService;
import au.edu.educationau.opensource.dsm.util.Flog;
import au.edu.educationau.opensource.dsm.worker.SearchAdapterFactory;
import au.edu.educationau.opensource.dsm.worker.SearchResultSorter;

/**
 * Holds the search results of all the sources, thesauri lookups, spelling
 * suggestions and other statistical data.
 */

public class SearchResultSet extends java.lang.Object implements java.io.Serializable {
	/** Timestamp at which this object was made - checked in the purging thread */
	private long timeCreated = 0;

	/** A table of results indexed by source */
	private Hashtable results = new Hashtable();

	/** Timestamp when it was last accessed - checked in the purging thread */
	private Date lastAccessed;

	/** The total number of results unfiltered */
	private int rawResultCount = 0;

	/** The number of results in this set after filtering */
	private int setResultCount = 0;

	/** The array of sources from which results are expected */
	private String[] sources = null;

	/** The table of durations that each search task took */
	private Hashtable taskTimes = null;

	/** The total time taken to complete the search job */
	private long totalTimeTaken = 0; // milliseconds is really the time taken
										// by the longest task

	/** Flag for completed search job */
	private boolean completed = false;

	/** Spelling corrections */

	private String correctedTerms = "";

	/**
	 * The total number of theoretical results available (total that could be
	 * accessed without limits) for all the repositories
	 */
	private int totalTheoreticalResultsAvailable = 0;

	/**
	 * Table of theoretical results available (total that could be accessed
	 * without limits) by source id
	 */
	private Hashtable theoreticalResultsAvailable = null;

	/** Array of narrower terms brought back by the thesauri tasks */
	private String[] narrowerTerms = new String[0];

	/** Array of like terms brought back by the thesauri tasks */
	private String[] likeTerms = new String[0];

	/** Array of broader terms brought back by the thesauri tasks */
	private String[] broaderTerms = new String[0];

	/** The table of responseCodes that each search task received */
	private Hashtable taskResponseCodes = null;

	/** The table of diagnostics that each search task received */
	private Hashtable taskDiagnostics = null;

	/** The table of diagnostics that each search task received */
	private Hashtable sourceFacets = new Hashtable();

	/**
	 * Constructor for the search result set. Requires the sources to be
	 * supplied up-front
	 * 
	 * @param sources
	 */
	public SearchResultSet(String[] sources) {
		lastAccessed = new Date();
		taskTimes = new Hashtable(sources.length);
		taskResponseCodes = new Hashtable(sources.length);
		taskDiagnostics = new Hashtable(sources.length);
		theoreticalResultsAvailable = new Hashtable(sources.length);
		for (int i = 0; i < sources.length; i++) {
			ArrayList adapterResults = new ArrayList(100); // assume an initial
															// average capacity
															// of 100 results
			results.put(sources[i], adapterResults);
			taskTimes.put(sources[i], new Long(0));
			theoreticalResultsAvailable.put(sources[i], new Integer(0));
			taskResponseCodes.put(sources[i], "200");
			taskDiagnostics.put(sources[i], "");
		}
		this.sources = sources;
		timeCreated = System.currentTimeMillis();
	}

	/**
	 * Add the results to the results table by source and update the theoretical
	 * Results available for this source
	 * 
	 * @param batch
	 * @param source
	 * @param theoreticalAvailableResults
	 */
	public void addResults(ArrayList batch, String source, int theoreticalAvailableResults, Map facetQueryCounts) {
		if (null != batch && batch.size() > 0) {
			theoreticalResultsAvailable.put(source, new Integer(theoreticalAvailableResults));
			sourceFacets.put(source, facetQueryCounts);
			this.totalTheoreticalResultsAvailable = theoreticalAvailableResults;
			((ArrayList) results.get(source)).addAll(batch);
			rawResultCount += batch.size();
		}
	}

	/** Get the unfiltered total number of results */
	public int getRawResultCount() {
		return rawResultCount;
	}

	/** Get the filtered total number of results */
	public int getSetResultCount() {
		return setResultCount;
	}

	/** Get the total number of milliseconds to complete this search job */
	public long getTotalTimeTaken() {
		return totalTimeTaken;
	}

	/** Get the total number of theoretical results available */
	public int getTotalTheoreticalResultsAvailable() {
		return totalTheoreticalResultsAvailable;
	}

	/**
	 * Gets the number of theoretical results available by source id
	 * 
	 * @param source
	 */
	public int getTheoreticalResultsAvailable(String source) {
		int sourceResults = rawResultCount;

		try {
			sourceResults = ((Integer) theoreticalResultsAvailable.get(source)).intValue();
		} catch (Exception oe) {
			sourceResults = rawResultCount;
		}

		return sourceResults;
	}

	/**
	 * Get the time taken in milliseconds to get the results from a source
	 * 
	 * @param source
	 */
	public long getTimeTakenBySource(String source) {
		return ((Long) taskTimes.get(source)).longValue();
	}

	/**
	 * Get the response code of a source
	 * 
	 * @param source
	 */
	public String getResponseCodeOfSource(String source) {
		return (String) taskResponseCodes.get(source);
	}

	/**
	 * Get the diagnostic of a source
	 * 
	 * @param source
	 */
	public String getDiagnosticOfSource(String source) {
		return (String) taskDiagnostics.get(source);
	}

	/**
	 * Calculates the total time taken for a source. It normalises the value to
	 * the total time to prevent non-sensible values.
	 * 
	 * @param time
	 * @param source
	 */
	public void calculateTotalTime(long time, String source) {
		if (time > this.totalTimeTaken) {
			this.totalTimeTaken = time;
		}
		if (null != source) {
			taskTimes.put(source, new Long(time));
		}
	}

	/**
	 * Sets the response code for a source
	 * 
	 * @param source
	 * @param code
	 */
	public void setResponseCode(String source, String code) {
		if (null == code || code.length() <= 0) {
			code = "500";
		}
		taskResponseCodes.put(source, code);
	}

	/**
	 * Sets the diagnostic response for a source
	 * 
	 * @param source
	 * @param code
	 */
	public void setDiagnostic(String source, String diagnostic) {
		taskDiagnostics.put(source, diagnostic);
	}

	/**
	 * Set the map containg facet query counts. The Map keys are a string of
	 * form <i>field:expression</i>; values are Integer objects representing
	 * the count.
	 * 
	 * <p>
	 * An expression can be either a single value or a range represented in
	 * Lucene query format.
	 * 
	 * @param facetQueryCounts
	 *            the map containing
	 */
	public void setFacetQueryCounts(String source, Map facetQueryCounts) {
		sourceFacets.put(source, facetQueryCounts);
	}

	/**
	 * Returns the query->count map representing facets for this source.
	 * 
	 * @param source
	 * @return the query->count map representing facets for this source
	 */
	public Map getFacetQueryCounts(String source) {
		return (Map) sourceFacets.get(source);
	}

	/**
	 * Update the thesaurus terms stores with the found terms
	 * 
	 * @param narrowerTerms
	 * @param likeTerms
	 * @param broaderTerms
	 */
	public void updateThesauriTerms(String[] narrowerTerms, String[] likeTerms, String[] broaderTerms) {
		this.narrowerTerms = narrowerTerms;
		this.likeTerms = likeTerms;
		this.broaderTerms = broaderTerms;
	}

	/**
	 * Set the correctedTerms value from the spelling suggestion task
	 * 
	 * @param correctedTerms
	 */
	public void updateCorrectedTerms(String correctedTerms) {
		this.correctedTerms = correctedTerms;
	}

	/**
	 * Returns the narrower Thesaurus Terms
	 */
	public String[] getNarrowerTerms() {
		return narrowerTerms;
	}

	/**
	 * Returns the like Thesaurus Terms
	 */
	public String[] getLikeTerms() {
		return likeTerms;
	}

	/**
	 * Returns the broader Thesaurus Terms
	 */
	public String[] getBroaderTerms() {
		return broaderTerms;
	}

	/**
	 * Returns the spelling corrected Terms
	 */
	public String getCorrectedTerms() {
		return this.correctedTerms;
	}

	/**
	 * Returns all the results stored in this SearchResultSet
	 */
	public SearchResult[] getAllResults() {
		lastAccessed = new Date();
		ArrayList list = new ArrayList();
		Enumeration sources = results.keys();
		while (sources.hasMoreElements()) {
			String source = (String) sources.nextElement();
			list.addAll((Collection) results.get(source));
		}
		return (SearchResult[]) list.toArray(new SearchResult[list.size()]);
	}

	/**
	 * Returns the results by a particular source.
	 * 
	 * @param start
	 *            The result index to start at
	 * @param pagesize
	 *            The number of results per page on the web tier
	 * @param source
	 *            The source id
	 * @param sortType
	 *            The sort algorithm
	 */
	public SearchResult[] getResultsbySource(int start, int pagesize, String source, String sortType) {
		lastAccessed = new Date();
		// SORT Results
		SearchResult[] sortedResults = new SearchResult[0];
		try {
			ArrayList list = (ArrayList) results.get(source.toLowerCase());
			if (null != list) {
				this.setResultCount = list.size();
				if (sortType.equals(SearchResultSorter.SORTED_BY_PRIORITY_INTERLEAVED)) {
					sortType = SearchResultSorter.SORTED_BY_RELEVANCE; // default
																		// to
																		// relevance
																		// if
																		// interleaving
																		// is
																		// accidentally
																		// chosen.
				} else if (sortType.equals(SearchResultSorter.SORTED_BY_PRIORITY_INTERLEAVED_DROP)) {
					sortType = SearchResultSorter.SORTED_BY_RELEVANCE_DROP; // default
																			// to
																			// relevance
																			// drop
																			// if
																			// interleaving
																			// drop
																			// is
																			// accidentally
																			// chosen.
				}
				sortedResults = SearchResultSorter.sortBy(list, sortType, start, pagesize);
			}
		} catch (Exception oe) {
			Flog.error(toString(), "Unable to sort results", oe);
		}
		return sortedResults;
	}

	/**
	 * Returns the result count by a particular source.
	 * 
	 * @param source
	 *            The source id - edna, gem etc
	 * @return the number of results a source has
	 */
	public int getResultCountbySource(String source) {
		ArrayList list = (ArrayList) results.get(source.toLowerCase());
		int resultCount = 0;
		if (null != list) {
			resultCount = list.size();
		}
		return resultCount;
	}

	/**
	 * Return the Most Relevant Results - for the first tab of combined results
	 * 
	 * @param start
	 *            The result index to start at
	 * @param pagesize
	 *            The number of results per page on the web tier
	 * @param maxResults
	 *            The number maximum number of results in this tab
	 * @param sortType
	 *            sortType The sort algorithm
	 * @param allowDuplicates
	 *            Flag to cull duplicate results from different repositories
	 */
	public SearchResult[] getDistributedResults(int start, int pagesize, int maxResults, String sortType, boolean allowDuplicates) {
		return getDistributedResults(start, pagesize, maxResults, sortType, allowDuplicates, null);
	}

	/**
	 * getDistributedResults - returns the Most Relevant Results list with
	 * sorting applied.
	 * 
	 * Notes:
	 *  - Algorithm for interleaving by source priorities
	 * 
	 * Build the result list by taking the most relevant results out of each
	 * source pool by the source's priority. The priority is set in the web end
	 * via priority.X=Y where X is the source id and Y is an integer. The
	 * default priority is found in the dsm4.properties file.
	 * 
	 * @param start -
	 *            the result number to start with
	 * @param pagesize -
	 *            the page size
	 * @param maxResults -
	 *            the size of the results to limit to
	 * @param sortType -
	 *            as defined constants in SearchResultSorter
	 * @param allowDuplicates -
	 *            allow duplicates within the result set. A duplicate is a
	 *            result with the same URL
	 * @param prioritisedSources -
	 *            sources prefixed by 'index.' when using
	 *            SORTED_BY_PRIORITY_INTERLEAVED
	 * 
	 * @return An array of SearchResult objects
	 * 
	 */
	public SearchResult[] getDistributedResults(int start, int pagesize, int maxResults, String sortType, boolean allowDuplicates, String[] prioritisedSources) {
		lastAccessed = new Date();
		if (sources.length > 0) {
			ArrayList list = new ArrayList();

			if (PropertiesService.getBooleanProperty("use.result.mix.factor", false)) {
				int[] resultMixFactors = new int[sources.length];
				int totalResultMixFactors = 0;
				for (int i = 0; i < sources.length; i++) {
					resultMixFactors[i] = SearchAdapterFactory.getAdapterProperties(sources[i]).getResultMixFactor();
					totalResultMixFactors += resultMixFactors[i];
				}

				int[] requiredNumbers = new int[sources.length];
				for (int i = 0; i < sources.length; i++) {
					requiredNumbers[i] = (int) Math.ceil(((double) resultMixFactors[i] / (double) totalResultMixFactors) * (double) maxResults);
				}

				for (int i = 0; i < sources.length; i++) {
					ArrayList adapterList = (ArrayList) results.get(sources[i]);
					if (adapterList.size() < requiredNumbers[i]) {
						if (!allowDuplicates) {
							for (int a = 0; a < adapterList.size(); a++) {
								if (!list.contains(adapterList.get(a))) {
									list.add(adapterList.get(a));
								}
							}
						} else {
							list.addAll(adapterList);
						}
					} else {
						if (!allowDuplicates) {
							for (int a = 0; a < requiredNumbers[i]; a++) {
								if (!list.contains(adapterList.get(a))) {
									list.add(adapterList.get(a));
								}
							}
						} else {
							list.addAll(adapterList.subList(0, requiredNumbers[i]));
						}
					}
				}
			} else {
				// NO RESULT MIX FACTOR
				if (sortType.equals(SearchResultSorter.SORTED_BY_PRIORITY_INTERLEAVED) && null != prioritisedSources) {
					doPriorityInterleave(maxResults, allowDuplicates, prioritisedSources, list);
				} else if (sortType.equals(SearchResultSorter.SORTED_BY_PRIORITY_INTERLEAVED_DROP) && null != prioritisedSources) {
					doPriorityInterleaveDrop(maxResults, allowDuplicates, prioritisedSources, list);
				} else {
					for (int i = 0; i < sources.length; i++) {
						ArrayList adapterResultList = (ArrayList) results.get(sources[i]);
						if (!allowDuplicates) {
							for (int a = 0; a < adapterResultList.size(); a++) {
								if (!list.contains(adapterResultList.get(a))) {
									list.add(adapterResultList.get(a));
								}
							}
						} else {
							list.addAll(adapterResultList);
						}
					}
				}
			}

			if (list.size() > maxResults) {
				this.setResultCount = maxResults;
			} else {
				this.setResultCount = list.size();
			}

			// SORT Results
			SearchResult[] sortedResults = new SearchResult[0];
			try {
				sortedResults = SearchResultSorter.sortBy(list, sortType, start, pagesize);
			} catch (Exception oe) {
				Flog.error(toString(), "Unable to sort results", oe);
			}

			return sortedResults;
		} else {
			ArrayList list = (ArrayList) results.get(sources[0]);
			return (SearchResult[]) list.toArray(new SearchResult[list.size()]);
		}
	}

	/**
	 * Interleave results according to priorities as specified.
	 */
	private void doPriorityInterleave(int maxResults, boolean allowDuplicates, String[] prioritisedSources, ArrayList list) {
		// Do interleaving here
		SearchResult result = null;
		ArrayList adapterResultList = null;

		for (int i = 0; i < maxResults; i++) {
			for (int j = 0; j < prioritisedSources.length; j++) {
				adapterResultList = (ArrayList) results.get(prioritisedSources[j].substring(prioritisedSources[j].indexOf(".") + 1));
				if (null != adapterResultList && i < adapterResultList.size()) {
					result = (SearchResult) adapterResultList.get(i);
					if ((!allowDuplicates && !list.contains(result)) || allowDuplicates) {
						list.add(result);
					}
				}
			} // for - iterating prioritisedSources to get largest batch size
		} // for - build list
	}

	/**
	 * Interleave results according to priorities as specified, additionally
	 * dropping items with negative priority and items without link, description
	 * or title to the end.
	 */
	private void doPriorityInterleaveDrop(int maxResults, boolean allowDuplicates, String[] prioritisedSources, ArrayList list) {

		// separate positive and negative priorities
		List positiveSources = new ArrayList();
		List negativeSources = new ArrayList();
		for (int i = 0; i < prioritisedSources.length; i++) {
			// string is in format <int-priority>.<source-name>
			int priority = NumberUtils.toInt(prioritisedSources[i].substring(0, prioritisedSources[i].indexOf(".")), 0);
			String source = prioritisedSources[i].substring(prioritisedSources[i].indexOf(".") + 1);
			if (priority < 0) {
				negativeSources.add(source);
			} else {
				positiveSources.add(source);
			}
		}

		// maintain map of incomplete items keyed on source
		Map incompleteResults = new HashMap();

		// interleave complete items for positive-priority sources; incomplete
		// items are held until later
		if (!positiveSources.isEmpty()) {
			interleaveOrDropResults(positiveSources, results, list, incompleteResults, maxResults, allowDuplicates, true);
		}

		// interleave complete items for negative-priority sources; incomplete
		// items are held until later
		if (!negativeSources.isEmpty()) {
			interleaveOrDropResults(negativeSources, results, list, incompleteResults, maxResults, allowDuplicates, true);
		}

		// TODO - should sort incompleteResults lists by 'relevance-drop' now

		// interleave incomplete items for positive-priority sources
		if (!positiveSources.isEmpty() && !incompleteResults.isEmpty()) {
			interleaveOrDropResults(positiveSources, incompleteResults, list, null, maxResults, allowDuplicates, false);
		}

		// interleave incomplete items for positive-priority sources
		if (!negativeSources.isEmpty() && !incompleteResults.isEmpty()) {
			interleaveOrDropResults(negativeSources, incompleteResults, list, null, maxResults, allowDuplicates, false);
		}
	}

	/**
	 * Priority interleave results, dropping nominated items. This is a worker
	 * method for doPriorityInterleaveDrop.
	 * 
	 * @param prioritisedSources
	 *            list of source names in order for interleaving
	 * @param resultsMap
	 *            source map of lists of results, keyed on source
	 * @param targetList
	 *            interleaved results are put in here
	 * @param droppedResults
	 *            dropped results are put in here if passed; a map of lists of
	 *            results, keyed on source
	 * @param maxResults
	 *            fill the targetList to this size
	 * @param allowDuplicates
	 *            check for duplicates in target list before adding
	 * @param dropIncompleteResults
	 *            if true incomplete results are dropped
	 */
	private void interleaveOrDropResults(List prioritisedSources, Map resultsMap, ArrayList targetList, Map droppedResults, int maxResults, boolean allowDuplicates, boolean dropIncompleteResults) {

		ArrayList sources = new ArrayList(prioritisedSources);
		int[] sourceIndexes = new int[sources.size()];
		ArrayList sourceResults;
		SearchResult result;

		while (!sources.isEmpty() && targetList.size() < maxResults) {
			loopSources: for (int i = 0; i < sources.size(); i++) {
				sourceResults = (ArrayList) resultsMap.get(sources.get(i));

				if (sourceResults == null || sourceIndexes[i] >= sourceResults.size()) {
					// there are no more valid results for this source
					sourceIndexes = ArrayUtils.remove(sourceIndexes, i);
					sources.remove(i);
					i--;
					continue loopSources;
				}

				while (sourceIndexes[i] < sourceResults.size()) {
					result = (SearchResult) sourceResults.get(sourceIndexes[i]);
					sourceIndexes[i]++;
					if ((!allowDuplicates && !targetList.contains(result)) || allowDuplicates) {
						if (!dropIncompleteResults || (dropIncompleteResults && isResultComplete(result))) {
							targetList.add(result);
							continue loopSources;
						} else if (droppedResults != null) {
							addToSourceKeyedMap(droppedResults, result);
						}
					}
				} // loop single source
			} // loop sources
		}
	}

	/**
	 * Checks whether a search result is complete WRT PriorityInterleaveDrop
	 * sorting.
	 */
	private boolean isResultComplete(SearchResult result) {
		return (result.getLink() != null && result.getLink().length() > 0 && result.getDescription() != null && result.getDescription().length() > 0 && result.getTitle() != null && result.getTitle().length() > 0);
	}

	/**
	 * Adds a result to List in a map keyed on source, creating the list if
	 * needed.
	 */
	private void addToSourceKeyedMap(Map map, SearchResult result) {
		List list = (List) map.get(result.getSource());
		if (list == null) {
			list = new ArrayList();
			map.put(result.getSource(), list);
		}
		list.add(result);
	}

	/** Check if the job has been completed */
	public boolean isCompletedSet() {
		return this.completed;
	}

	/** Set the completed flag */
	public void flagComplete() {
		this.completed = true;
	}

	/** Returns the timestamp this set was created */
	public long getTimeCreated() {
		return this.timeCreated;
	}

	/** Class name display */
	public String toString() {
		return "o.m.d.o.SearchResultSet";
	}

	public String[] getSources() {
		return sources;
	}

	public void setSources(String[] sources) {
		this.sources = sources;
	}

} // class SearchResultSet

// -- EOF
